<?php


namespace Sjj\Eloquent;


use ArrayAccess;
use Exception;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Support\Jsonable;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\JsonEncodingException;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
use JsonSerializable;

/**
 * Class ViewModel
 * @package Sjj\Eloquent
 */
abstract class ViewModel implements ArrayAccess, Arrayable, Jsonable, JsonSerializable
{
	use Concerns\HasAttributes,
		Concerns\HasRelationships,
		Concerns\HidesAttributes;

	/**
	 * The table associated with the model.
	 *
	 * @var string
	 */
	protected $table;

	/**
	 * The primary key for the model.
	 *
	 * @var string
	 */
	protected $primaryKey = 'id';

	/**
	 * The "type" of the auto-incrementing ID.
	 *
	 * @var string
	 */
	protected $keyType = 'int';

	/**
	 * The relations to eager load on every query.
	 *
	 * @var array
	 */
	protected $with = [];

	/**
	 * Create a new Eloquent model instance.
	 *
	 * @param Model|null $model
	 */
	public function __construct(?Model $model)
	{
		if ($model) {
			$this->fill($model->getAttributes());
		}
	}

	/**
	 * Fill the model with an array of attributes.
	 *
	 * @param array $attributes
	 * @return $this
	 */
	public function fill(array $attributes)
	{
		foreach ($attributes as $key => $value) {
			$this->setAttribute($key, $value);
		}
		return $this;
	}

	/**
	 * Eager load relations on the model.
	 *
	 * @param array|string $relations
	 * @return $this
	 */
	public function load($relations)
	{
		$relations = is_string($relations) ? func_get_args() : $relations;

		return $this;
	}

	/**
	 * Convert the model instance to an array.
	 *
	 * @return array
	 * @throws Exception
	 */
	public function toArray()
	{
		return array_merge($this->attributesToArray(), $this->relationsToArray());
	}

	/**
	 * Convert the model instance to JSON.
	 *
	 * @param int $options
	 * @return string
	 *
	 * @throws \Illuminate\Database\Eloquent\JsonEncodingException
	 * @throws Exception
	 */
	public function toJson($options = 0)
	{
		$json = json_encode($this->jsonSerialize(), $options);

		if (JSON_ERROR_NONE !== json_last_error()) {
			throw JsonEncodingException::forModel($this, json_last_error_msg());
		}

		return $json;
	}

	/**
	 * Convert the object into something JSON serializable.
	 *
	 * @return array
	 * @throws Exception
	 */
	public function jsonSerialize()
	{
		return $this->toArray();
	}

	/**
	 * Get the table associated with the model.
	 *
	 * @return string
	 */
	public function getTable()
	{
		if (!isset($this->table)) {
			return str_replace(
				'\\', '', Str::snake(Str::plural(class_basename($this)))
			);
		}

		return $this->table;
	}

	/**
	 * Get the value of the model's primary key.
	 *
	 * @return mixed
	 * @throws Exception
	 */
	public function getKey()
	{
		return $this->getAttribute($this->getKeyName());
	}

	/**
	 * Get the primary key for the model.
	 *
	 * @return string
	 */
	public function getKeyName()
	{
		return $this->primaryKey;
	}

	/**
	 * Get the auto-incrementing key type.
	 *
	 * @return string
	 */
	public function getKeyType()
	{
		return $this->keyType;
	}

	/**
	 * Dynamically retrieve attributes on the model.
	 *
	 * @param string $key
	 * @return mixed
	 * @throws Exception
	 */
	public function __get($key)
	{
		return $this->getAttribute($key);
	}

	/**
	 * Dynamically set attributes on the model.
	 *
	 * @param string $key
	 * @param mixed $value
	 * @return void
	 * @throws Exception
	 */
	public function __set($key, $value)
	{
		$this->setAttribute($key, $value);
	}

	/**
	 * Determine if the given attribute exists.
	 *
	 * @param mixed $offset
	 * @return bool
	 * @throws Exception
	 */
	public function offsetExists($offset)
	{
		return !is_null($this->getAttribute($offset));
	}

	/**
	 * Get the value for a given offset.
	 *
	 * @param mixed $offset
	 * @return mixed
	 * @throws Exception
	 */
	public function offsetGet($offset)
	{
		return $this->getAttribute($offset);
	}

	/**
	 * Set the value for a given offset.
	 *
	 * @param mixed $offset
	 * @param mixed $value
	 * @return void
	 * @throws Exception
	 */
	public function offsetSet($offset, $value)
	{
		$this->setAttribute($offset, $value);
	}

	/**
	 * Unset the value for a given offset.
	 *
	 * @param mixed $offset
	 * @return void
	 */
	public function offsetUnset($offset)
	{
		unset($this->attributes[$offset], $this->relations[$offset]);
	}

	/**
	 * Determine if an attribute or relation exists on the model.
	 *
	 * @param string $key
	 * @return bool
	 * @throws Exception
	 */
	public function __isset($key)
	{
		return $this->offsetExists($key);
	}

	/**
	 * Unset an attribute on the model.
	 *
	 * @param string $key
	 * @return void
	 */
	public function __unset($key)
	{
		$this->offsetUnset($key);
	}

	/**
	 * Handle dynamic method calls into the model.
	 *
	 * @param string $method
	 * @param array $parameters
	 * @return mixed
	 */
	public function __call($method, $parameters)
	{
		return $this->$method(...$parameters);
	}

	/**
	 * Convert the model to its string representation.
	 *
	 * @return string
	 * @throws Exception
	 */
	public function __toString()
	{
		return $this->toJson();
	}
}
